Ensure execution order of callbacks that are expected to be called immediately (such as call_soon)#123
Ensure execution order of callbacks that are expected to be called immediately (such as call_soon)#123gampixi wants to merge 3 commits intoCabbageDevelopment:masterfrom
Conversation
|
Hang on, I've rushed with creating this PR a bit. This approach causes excessive CPU usage due to excessive polling. It should be straightforward to mitigate that, though. Edit: commit 797c8f6 should fix that. The timer now gets created on demand, but the general idea that all immediate callbacks are serviced by a single (rather than separate per-callback) timer remains. |
…them independently of delayed callbacks Under heavy UI load (such as real-time plotting with pyqtgraph), it was observed that the zero-delay timers scheduled via _SimpleTimer would occasionally run out-of-order on Windows.
…ling induced CPU usage
|
Thanks for submitting this @gampixi! I think it would be good to conform to asyncio here as well. I am however, getting a failure on the |
|
Thanks for taking a look at the PR, unfortunately I won't be able to dedicate additional attention to this in the near future. Obviously, the test must be fixed. Benchmarking sounds like a fair idea. Anecdotally I can say that we've been using this patch for our internal tools for a while with no observed performance problems (the same use case as described earlier, with real-time data streaming). |
Summary
Ensure execution order of callbacks with zero delay (such as call_soon) by handling them independently of delayed callbacks.
It's reasonable to assume that custom event loops would adhere to the contract established in the
asynciodocs. Quoting the asyncio docs:This PR accomplishes it by running a separate timer (with 0 timeout) for servicing immediate callbacks. According to Qt documentation, this timer will run each time the Qt event loop finishes servicing window events (https://doc.qt.io/qt-6/qtimer.html#interval-prop), which should ensure responsive operation.
Issue was observed on Windows.
I hope someone more versed in asyncio and Qt can chime in here. I see this fix more as a best-guess workaround, as I didn't dive into why the 0-delay timer callbacks would get executed out-of-order in the first place. Some quick research didn't give clear clues on whether we can expect any execution order guarantees from the original method.
Some background
Under heavy UI load (such as real-time plotting with pyqtgraph), it was observed that the zero-delay timers scheduled via _SimpleTimer would occasionally run out-of-order on Windows. In these cases multiple immediate callbacks would be serviced within a single event loop iteration.
Unfortunately I haven't been able to get a minimum reproducible example working despite my best efforts to artificially abuse the event loop, but I'll give some overview on how the issue was observed.
We have an application that reads a realtime data stream from a BLE device (using Bleak). New values may be received up to 400 times per second, with each update causing Bleak to use
call_soonto queue the processing of the value.As observed, each update roughly goes through these stages:
call_soon_threadsafe, which makes Qt emit the passed callback as a signalcall_soon(from a different thread than Bleak used)call_soontocall_laterwithdelay=0call_laterbuilds anasyncio.Handleand passes it to_add_callback_add_callbackredirects to the_SimpleTimer, which registers a timer with a timeout of 0timerEventmethodThe out-of-order issue was observed by adding tracing to each respective stage, where the 6th stage would occasionally call the callbacks out of order, like so: [1, 2, 3, 4, 6, 5, 7, 8, ...]. No callbacks were lost.
This would only happen when the app has heavy load, such as when a graph update took longer than several milliseconds.
After applying the workaround from this PR, the issue no longer happens.